/*
 * $Id: JXImagePanel.java 4017 2011-05-10 21:00:48Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx;

import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;

/**
 * <p>
 * A panel that draws an image. The standard mode is to draw the specified image
 * centered and unscaled. The component&amp;s preferred size is based on the
 * image, unless explicitly set by the user.
 * </p>
 * <p>
 * Images to be displayed can be set based on URL, Image, etc. This is
 * accomplished by passing in an image loader.
 * 
 * <pre>
 * public class URLImageLoader extends Callable&lt;Image&gt; {
 * 	private URL url;
 * 
 * 	public URLImageLoader(URL url) {
 * 		url.getClass(); // null check
 * 		this.url = url;
 * 	}
 * 
 * 	public Image call() throws Exception {
 * 		return ImageIO.read(url);
 * 	}
 * }
 * 
 * imagePanel.setImageLoader(new URLImageLoader(url));
 * </pre>
 * 
 * </p>
 * <p>
 * This component also supports allowing the user to set the image. If the
 * <code>JXImagePanel</code> is editable, then when the user clicks on the
 * <code>JXImagePanel</code> a FileChooser is shown allowing the user to pick
 * some other image to use within the <code>JXImagePanel</code>.
 * </p>
 * <p>
 * TODO In the future, the JXImagePanel will also support tiling of images,
 * scaling, resizing, cropping, segues etc.
 * </p>
 * <p>
 * TODO other than the image loading this component can be replicated by a
 * JXPanel with the appropriate Painter. What's the point?
 * </p>
 * 
 * @author rbair
 * @deprecated (pre-1.6.2) use a JXPanel with an ImagePainter; see Issue 988
 */
// moved to package-private instead of deleting; needed by JXLoginPane
@Deprecated
class JXImagePanel extends JXPanel {
	public static enum Style {
		CENTERED, TILED, SCALED, SCALED_KEEP_ASPECT_RATIO
	}

	private static final Logger LOG = Logger.getLogger(JXImagePanel.class.getName());

	/**
	 * Text informing the user that clicking on this component will allow them
	 * to set the image
	 */
	private static final String TEXT = "<html><i><b>Click here<br>to set the image</b></i></html>";

	/**
	 * The image to draw
	 */
	private SoftReference<Image> img = new SoftReference<Image>(null);

	/**
	 * If true, then the image can be changed. Perhaps a better name is
	 * &quot;readOnly&quot;, but editable was chosen to be more consistent with
	 * other Swing components.
	 */
	private boolean editable = false;

	/**
	 * The mouse handler that is used if the component is editable
	 */
	private MouseHandler mhandler = new MouseHandler();

	/**
	 * Specifies how to draw the image, i.e. what kind of Style to use when
	 * drawing
	 */
	private Style style = Style.CENTERED;

	private Image defaultImage;

	private Callable<Image> imageLoader;

	private static final ExecutorService service = Executors.newFixedThreadPool(5);

	public JXImagePanel() {
	}

	// TODO remove this constructor; no where else can a URL be used in this
	// class
	public JXImagePanel(URL imageUrl) {
		try {
			setImage(ImageIO.read(imageUrl));
		} catch (Exception e) {
			// TODO need convert to something meaningful
			LOG.log(Level.WARNING, "", e);
		}
	}

	/**
	 * Sets the image to use for the background of this panel. This image is
	 * painted whether the panel is opaque or translucent.
	 * 
	 * @param image
	 *            if null, clears the image. Otherwise, this will set the image
	 *            to be painted. If the preferred size has not been explicitly
	 *            set, then the image dimensions will alter the preferred size
	 *            of the panel.
	 */
	public void setImage(Image image) {
		if (image != img.get()) {
			Image oldImage = img.get();
			img = new SoftReference<Image>(image);
			firePropertyChange("image", oldImage, img);
			invalidate();
			repaint();
		}
	}

	/**
	 * @return the image used for painting the background of this panel
	 */
	public Image getImage() {
		Image image = img.get();

		// TODO perhaps we should have a default image loader?
		if (image == null && imageLoader != null) {
			try {
				image = imageLoader.call();
				img = new SoftReference<Image>(image);
			} catch (Exception e) {
				LOG.log(Level.WARNING, "", e);
			}
		}
		return image;
	}

	/**
	 * @param editable
	 */
	public void setEditable(boolean editable) {
		if (editable != this.editable) {
			// if it was editable, remove the mouse handler
			if (this.editable) {
				removeMouseListener(mhandler);
			}
			this.editable = editable;
			// if it is now editable, add the mouse handler
			if (this.editable) {
				addMouseListener(mhandler);
			}
			setToolTipText(editable ? TEXT : "");
			firePropertyChange("editable", !editable, editable);
			repaint();
		}
	}

	/**
	 * @return whether the image for this panel can be changed or not via the
	 *         UI. setImage may still be called, even if <code>isEditable</code>
	 *         returns false.
	 */
	public boolean isEditable() {
		return editable;
	}

	/**
	 * Sets what style to use when painting the image
	 * 
	 * @param s
	 */
	public void setStyle(Style s) {
		if (style != s) {
			Style oldStyle = style;
			style = s;
			firePropertyChange("style", oldStyle, s);
			repaint();
		}
	}

	/**
	 * @return the Style used for drawing the image (CENTERED, TILED, etc).
	 */
	public Style getStyle() {
		return style;
	}

	/**
	 * {@inheritDoc} The old property value in PCE fired by this method might
	 * not be always correct!
	 */
	@Override
	public Dimension getPreferredSize() {
		if (!isPreferredSizeSet() && img != null) {
			Image img = this.img.get();
			// was img GCed in the mean time?
			if (img != null) {
				// it has not been explicitly set, so return the width/height of
				// the image
				int width = img.getWidth(null);
				int height = img.getHeight(null);
				if (width == -1 || height == -1) {
					return super.getPreferredSize();
				}
				Insets insets = getInsets();
				width += insets.left + insets.right;
				height += insets.top + insets.bottom;
				return new Dimension(width, height);
			}
		}
		return super.getPreferredSize();
	}

	/**
	 * Overridden to paint the image on the panel
	 * 
	 * @param g
	 */
	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		Graphics2D g2 = (Graphics2D) g;
		Image img = this.img.get();
		if (img == null && imageLoader != null) {
			// schedule for loading (will repaint itself once loaded)
			// have to use new future task every time as it holds strong
			// reference to the object it retrieved and doesn't allow to reset
			// it.
			service.execute(new FutureTask<Image>(imageLoader) {

				@Override
				protected void done() {
					super.done();

					SwingUtilities.invokeLater(new Runnable() {
						@Override
						public void run() {
							try {
								JXImagePanel.this.setImage(get());
							} catch (InterruptedException e) {
								// ignore - canceled image load
							} catch (ExecutionException e) {
								LOG.log(Level.WARNING, "", e);
							}
						}
					});
				}

			});
			img = defaultImage;
		}
		if (img != null) {
			final int imgWidth = img.getWidth(null);
			final int imgHeight = img.getHeight(null);
			if (imgWidth == -1 || imgHeight == -1) {
				// image hasn't completed loading, return
				return;
			}

			Insets insets = getInsets();
			final int pw = getWidth() - insets.left - insets.right;
			final int ph = getHeight() - insets.top - insets.bottom;

			switch (style) {
				case CENTERED:
					Rectangle clipRect = g2.getClipBounds();
					int imageX = (pw - imgWidth) / 2 + insets.left;
					int imageY = (ph - imgHeight) / 2 + insets.top;
					Rectangle r = SwingUtilities.computeIntersection(imageX, imageY, imgWidth, imgHeight, clipRect);
					if (r.x == 0 && r.y == 0 && (r.width == 0 || r.height == 0)) {
						return;
					}
					// I have my new clipping rectangle "r" in clipRect space.
					// It is therefore the new clipRect.
					clipRect = r;
					// since I have the intersection, all I need to do is adjust
					// the
					// x & y values for the image
					int txClipX = clipRect.x - imageX;
					int txClipY = clipRect.y - imageY;
					int txClipW = clipRect.width;
					int txClipH = clipRect.height;

					g2.drawImage(img, clipRect.x, clipRect.y, clipRect.x + clipRect.width, clipRect.y + clipRect.height,
							txClipX, txClipY, txClipX + txClipW, txClipY + txClipH, null);
					break;
				case TILED:
					g2.translate(insets.left, insets.top);
					Rectangle clip = g2.getClipBounds();
					g2.setClip(0, 0, pw, ph);

					int totalH = 0;

					while (totalH < ph) {
						int totalW = 0;

						while (totalW < pw) {
							g2.drawImage(img, totalW, totalH, null);
							totalW += img.getWidth(null);
						}

						totalH += img.getHeight(null);
					}

					g2.setClip(clip);
					g2.translate(-insets.left, -insets.top);
					break;
				case SCALED:
					g2.drawImage(img, insets.left, insets.top, pw, ph, null);
					break;
				case SCALED_KEEP_ASPECT_RATIO:
					int w = pw;
					int h = ph;
					final float ratioW = ((float) w) / ((float) imgWidth);
					final float ratioH = ((float) h) / ((float) imgHeight);

					if (ratioW < ratioH) {
						h = (int) (imgHeight * ratioW);
					} else {
						w = (int) (imgWidth * ratioH);
					}

					final int x = (pw - w) / 2 + insets.left;
					final int y = (ph - h) / 2 + insets.top;
					g2.drawImage(img, x, y, w, h, null);
					break;
				default:
					LOG.fine("unimplemented");
					g2.drawImage(img, insets.left, insets.top, this);
					break;
			}
		}
	}

	/**
	 * Handles click events on the component
	 */
	private class MouseHandler extends MouseAdapter {
		private Cursor oldCursor;

		private JFileChooser chooser;

		@Override
		public void mouseClicked(MouseEvent evt) {
			if (chooser == null) {
				chooser = new JFileChooser();
			}
			int retVal = chooser.showOpenDialog(JXImagePanel.this);
			if (retVal == JFileChooser.APPROVE_OPTION) {
				File file = chooser.getSelectedFile();
				try {
					setImage(new ImageIcon(file.toURI().toURL()).getImage());
				} catch (Exception ex) {
				}
			}
		}

		@Override
		public void mouseEntered(MouseEvent evt) {
			if (oldCursor == null) {
				oldCursor = getCursor();
				setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
			}
		}

		@Override
		public void mouseExited(MouseEvent evt) {
			if (oldCursor != null) {
				setCursor(oldCursor);
				oldCursor = null;
			}
		}
	}

	public void setDefaultImage(Image def) {
		this.defaultImage = def;
	}

	public void setImageLoader(Callable<Image> loadImage) {
		this.imageLoader = loadImage;

	}
}
